#!/usr/bin/python
"""Configuration object for cherrypy server."""
import sys
import os
import re
import itertools
import subprocess

# Third-party modules
import cherrypy
from Cheetah.Template import Template
from httpauthfilter import HttpAuthFilter
import json

# Configmod-web modules
from uya.routerconf.configmod import config as configmod
from uya.routerconf.configmod.configobj import validate
from uya.routerconf.configmod.configobj import configobj
from uya.routerconf import validators
import html_helpers
import misc

def build_path(handler, path):
    """Return a string URL given a handler and a sequence path."""
    return "/" + "/".join([handler] + list(path))

def get_config_path(config, path):
    """Get a path from config given a sequence of sections."""
    return reduce(lambda subconfig, x: subconfig[x], path, config)

def get_view_info(config, path, key):
    """Get kwargs from view info."""
    try:    
        viewspec = get_config_path(config.view, path).configspec
        view = config.validator._parse_check(viewspec[key])[2]
    except KeyError:
        view = {}
    return view

def get_validate_info(config, path, key):
    """Return validation for a parameter.
    
    Template: parameter = string(min=1, max=3, default=2)
    View: parameter = view(help="some help")
    
    type = 'string'
    args = ()
    kwargs = {"min": 1, "max": 3}
    default = 2
    view = {"help", "some help"}
    """
    configspec = get_config_path(config, path).configspec
    validator = config.validator
    viewspec = get_config_path(config.view, path).configspec
    try:    
        type0, args, kwargs, default = validator._parse_check(configspec[key])
    except KeyError:
        type0, args, kwargs, default = None, [], {}, None
    view = get_view_info(config, path, key)
    return misc.Struct(
        type=type0, 
        args=args, 
        kwargs=kwargs, 
        default=default, 
        view=view,
    )

def expand_path(config, path):
    path = list(path)
    while 1:
        sections = get_config_path(config, path).sections
        if not sections:
            break
        path.append(sections[0])
    return path

def build_urls(config, validator, handler, path):
    """Return info to build URLs in view: sections, params and showinfo
    for selects."""  
    def _sections(path=path):
        #path = expand_path(config, path)
        for index in range(len(path)+1):            
            def _name_url(subsection, add_sections, parent_remove_sections):
                current_path0 = list(path[:index]) + [subsection]
                current_path = current_path0[:]
                if list(path[:index+1]) == list(path[:index]) + [subsection]:
                    url = None 
                else: url = build_path(handler, current_path)
                if not url and add_sections:
                    extra = "(%s)" % html_helpers.link("+", 
                        build_path("add", current_path0))                
                elif not url and parent_remove_sections:
                    extra = "(%s)" % html_helpers.link("-", 
                        build_path("remove", current_path0),
                        onclick="return confirm('Are you sure you want to remove section %s?');" % "/".join(current_path0))
                else: extra = None
                return subsection, url, extra
            view = get_view_info(config, path[:index], "__view__")
            parent_remove_sections = view.get("remove_sections")                
            sections = get_config_path(config, path[:index]).sections
            section_info = []
            for subsection in sections:
                path2 = list(path[:index]) + [subsection]
                config2 = get_config_path(config, path2)
                view = get_view_info(config, path2, "__view__")
                add_sections = view.get("add_sections")                
                max_sections = int(view.get("max_sections", 10))
                if add_sections and len(config2.sections) >= max_sections:
                    add_sections = None         
                if config2.sections or config2.scalars or add_sections:
                    section_info.append(_name_url(subsection, 
                        add_sections, parent_remove_sections))
            yield section_info
    def _parameters(groups):
        subconfig = get_config_path(config, path)
        def _groups_gen():
            if not groups:
                yield (None, [(k, subconfig[k]) for k in subconfig.scalars])
                return
            for group, params in groups:
                params2 = [(key, subconfig[key]) for key in params
                    if key in subconfig]
                yield (group, params2)
        for group, params in _groups_gen():
            info = []         
            for key, value in params:
                keyinfo = get_validate_info(config, path, key)
                info.append((path, key, value, keyinfo))
            yield (group, info)
    def _get_showinfo(groups):            
        subconfig = get_config_path(config, path)
        params_to_show = []
        dgroups = dict(groups)
        showinfo = {}  
        for key in subconfig.scalars:
            keyinfo = get_validate_info(config, path, key)
            if keyinfo.type not in ("option", "boolean"):
                continue
            if "show" in keyinfo.view:
                showinfo[key] = dict(get_select_onchange(path, 
                    parse_groups(keyinfo.view["show"])))
        return showinfo                    

    groups = list(parse_groups(get_view_info(config, path, "__view__").get("groups")))
    showinfo = _get_showinfo(groups)
    return filter(bool, _sections()), list(_parameters(groups)), showinfo

def parse_groups(s):
    """Parse a string of the form 'group1: item1 item2, group2: item3'"""
    if not s:
        return
    for group in s.split(","):
        group_name, strparams = map(str.strip, group.split(":"))
        yield group_name, strparams.split()

def pretty_key(key):
    """Return a pretty string from key name."""
    if not key:
        return key
    elif key and key[-1] in "0123456789":
        return key
    return key.replace("_", " ").title()

def build_config_id(path, key):
    """Return html ID attribute from key name."""
    return key

def get_select_onchange(section, key_showinfo):
    """Return IDs to show/hide when a select option changes its value."""    
    def _getid(key):
        if key.startswith("%"):
            return "group-%s" % key[1:]
        return build_config_id(section, key)
    for key, values in key_showinfo:
        toshow, tohide, tocheck = [], [], []
        for value in values:
            if value.startswith("-"):
               tohide.append(_getid(value[1:]))
            elif value.startswith("!"):
               tocheck.append(_getid(value[1:]))
            else: toshow.append(_getid(value))
        yield key, [toshow, tohide, tocheck]

def generic_input(section, key, value, keyinfo, errors, config, view_callbacks):
    """Return HTML for a key in a ConfigObj."""
    path = list(section) + [key]
    input_name = build_config_id(section, key)
    #kwargs = dict(id="param-"+input_name)
    kwargs = {}
    input_size = keyinfo.view.get("size")
    htmlfunc = keyinfo.view.get("htmlfunc")
    if input_size:
        kwargs["size"] = input_size
    sfields = keyinfo.view.get("fields", "")
    if htmlfunc:
        view_func = getattr(view_callbacks, htmlfunc)
        html = view_func(config, key, value)
    elif sfields:
        length, regexp, format = sfields.split("|")
        def _get_html_input(value):
            if value:
                try:
                    groups = re.match(regexp, str(value)).groups()
                except AttributeError:
                    groups = [""]*int(length)
            else: groups = [""]*int(length)
            fields = [html_helpers.input_tag("text", 
                input_name+"$"+"%02d" % index, value=value, 
                klass="field", size=2) 
                for (index, value) in enumerate(groups)]
            return format % tuple(fields)
        if keyinfo.type.endswith("_list"):
            html = html_helpers.text_list(input_name, value, _get_html_input, 
                "html_list_entry", id=input_name)
        else:
            html = _get_html_input(value)            
    elif keyinfo.type == "option":
        kwargs["onchange"] = "set_visibility(this); return false;"
        if keyinfo.view.get("radio"):
            html = html_helpers.radio_tag(input_name, keyinfo.args, value, **kwargs)
        else:
            html = html_helpers.select_tag(input_name, keyinfo.args, value, **kwargs)
    elif keyinfo.type == "boolean":
        #html = html_helpers.checkbox_tag(input_name, value, **kwargs)
        kwargs["onchange"] = "set_visibility(this); return false;"
        html = html_helpers.boolean_radio_tag(input_name, value, **kwargs)
    elif keyinfo.type and keyinfo.type.endswith("_list"):
        def _get_html_input(value):
            return html_helpers.input_tag("text", input_name, value=value,
                size=input_size)
        html = html_helpers.text_list(input_name, value, _get_html_input, 
            "html_list_entry", id=input_name)
    else:
        if value is None:
            value = ""
        html = html_helpers.input_tag("text", input_name, value=value, **kwargs)
    
    html = html_helpers.tag("div", html, klass="parameter-value")
    label_text0 = keyinfo.view.get("label", pretty_key(key))
    if label_text0:
        label_text = label_text0 + ":"
    else: label_text = ""  
    label = html_helpers.tag("label", label_text, 
        title=keyinfo.view.get("help", ""))
    if errors and path in errors:
        html = html_helpers.add_class_error(html, "field-error")
    return html_helpers.tag("div", label + html, id=input_name, klass="parameter") 

def validator_view(*args, **kwargs):
    pass
 
def group_parameters(params, config, path):
    """Group parameter fields.
    
    Params: {"par0": "1", "par1$0": "2", "par1$1": "3"}
    With view info for par1: groups='2|(\d+).(\d+)|%s.%s'
    
    Yield pairs ("par1", "1"), ("par1", "2.3").
    """  
    def _group_fields((k, v)):            
        match = re.match("^(.*)\$(\d+)$", k)
        if match:
            return match.group(1)
        return k
    for parameter, pairs in \
            itertools.groupby(sorted(params.items()), _group_fields):
        info = get_validate_info(config, path, parameter)
        values = [v for (k, v) in sorted(pairs)]
        if len(values) > 1:
            if info.type.endswith("_list"):
                values = values[1:]
                if values and not isinstance(values[0], list):
                    values_list = [tuple(values)]
                else:
                    values_list = zip(*filter(bool, values))
            else: values_list = [values]
            value = []
            for entry in values_list:
                if not misc.any(entry):
                    continue
                if not info.view or not info.view["fields"]:
                    raise ValueError, "No view group specified: %s (%s)" \
                        % (parameter, info.view)
                length, regexp, format = info.view["fields"].split("|")            
                value.append(format % tuple(entry))            
            if not info.type.endswith("_list"):
                if value:
                    value = value[0]
                else: value = ""
        else: 
            value = values[0]
        yield parameter, value                    

def load_config(configuration_file, template_file, view_file, validator):
    """Load config and validation."""
    config = configmod.load_config(configuration_file, template_file, validator)
    config.validator = validator
    config.view = configmod.load_config(configuration_file, 
        view_file, validator, ignore_errors=True)
    return config
           
class Configuration(object):
    
    def __init__(self, title, configuration_file, template_file, view_file, 
            html_templates_dir, callbacks, authlst, changed_hash_callback,
            view_callbacks):
        self.configuration_file = configuration_file
        self.template_file = template_file
        self.view_file = view_file
        self.html_templates_dir = html_templates_dir
        self.callbacks = callbacks
        self._render_namespace = None
        self.changed_hash_callback = changed_hash_callback
        self.view_callbacks = view_callbacks
        validators_dict = dict(
        route=validators.route,
            route_list=validators.route_list,
            ospf_network=validators.ospf_network,
            ospf_network_list=validators.ospf_network_list,
            ospf_area=validators.ospf_area,
            ospf_area_list=validators.ospf_area_list,
            view=validator_view,
        )
        self.validator = validate.Validator(validators_dict)
        self.constants = {
            "title": title,
        }
        self.set_authentication(authlst)
        self.notice = None
        
    def set_authentication(self, authlst):
        """Set authentication filter from list of pairs (user, password)."""     
        if authlst is None:
           return 
        cp_entry = HttpAuthFilter(
            realm='localhost', 
            privateKey='changethat',
            unauthorizedPath='/unauthorized', 
            retrieveUsersFunc=lambda: dict(authlst))
        self._cp_filters = [cp_entry]

    def run_callback(self, name):
        """Run a callback."""
        misc.run([self.callbacks[name]["script"]])

    def run_write(self):
        """Run write script (after save)."""
        command = self.callbacks["write"]["script"]
        print "run_write:", command
        args = [self.configuration_file, self.template_file]     
        misc.run([command]+args)

    def run_read(self):
        """Run read script to get configuration."""
        if not self.changed_hash_callback():
            print "configuration files not changed, don't spawn read process"
            return
        print "configuration files changed, spawn read process"
        command = self.callbacks["read"]["script"]
        args = [self.configuration_file, self.template_file]             
        retcode, outdata, errdata = misc.run([command]+args, 
            stderr=subprocess.PIPE)
        if retcode != 0:
            self.notice = errdata
        
    def render(self, layout, name, namespace={}, functions={}):
        """Render a page using a layout."""
        self._render_namespace = namespace
        def get_html(basename):
            path = os.path.join(self.html_templates_dir, basename+".html.cheetah")
            return open(path).read()
        default_namespace = {
            "title": self.constants["title"],
        }             
        namespace.update(default_namespace)
        # Save last namespace used (useful for easy controller testing)
        self.render_namespace = namespace        
        functions.update({   
            "pretty_key": pretty_key,
            "build_path": build_path,
        })
        contents = Template(get_html(name), 
            searchList=[functions, namespace]).respond()
        layout_namespace = dict(contents=contents)
        layout_namespace.update(default_namespace)
        html = get_html(os.path.join("layouts", layout))
        return Template(html, searchList=[functions, layout_namespace]).respond()
    
    # Public interface

    def namespace(self):
        return self._render_namespace

    def load_config(self):
        """Load real configuration, update configmod config and validate."""
        config = load_config(self.configuration_file, self.template_file, 
            self.view_file, self.validator)
        return config

    def render_admin(self, path, config, errors=None):
        """Render admin handler (show sections and parameters)."""
        path = expand_path(config, path)
        sections, parameters, showinfo = \
            build_urls(config, self.validator, "admin", path)
        namespace = {        
            "sections":sections,
            "parameters": parameters,
            "path": path,
            "errors": errors,
            "notice": self.notice,
            "showinfo": json.write(showinfo),
        }
        def generic_input_wrapper(*args):
            return generic_input(*(args+(config, self.view_callbacks,)))
        functions = {            
            "generic_input": generic_input_wrapper,
        }
        self.notice = None
        return self.render("default", "admin", namespace, functions)

    @cherrypy.expose
    def default(self, *path):
        self.run_read()
        config = self.load_config()
        if config.sections:
            raise cherrypy.HTTPRedirect(build_path("admin", [config.sections[0]]))

    @cherrypy.expose
    def reboot(self):
        # POST is accepted
        if cherrypy.request.method.upper() == "POST":
            print "rebooting"
            sudo.misc(["reboot"])
            self.notice = "Router will reboot, uya will be inaccesible for some time"
        raise cherrypy.HTTPRedirect("/status")

    @cherrypy.expose
    def status(self):
        self.run_read()
        config = self.load_config()
        sections, parameters, showinfo = \
            build_urls(config, self.validator, "admin", path=[])
        def text2html(s):
            return s.replace("\n", "<br />")
        namespace = {        
            "sections": sections,
            "notice": self.notice,
            "ifconfig": text2html(misc.run(["ifconfig"])[1]),
            "iwconfig": text2html(misc.run(["iwconfig"])[1]),
        }
        return self.render("default", "status", namespace, {})

    @cherrypy.expose
    def admin(self, *path):        
        self.run_read()
        config = self.load_config()
        if not path:
            raise cherrypy.HTTPRedirect(build_path("admin", [config.sections[0]]))
        try:
            get_config_path(config, path)
        except KeyError:
            raise cherrypy.HTTPRedirect(build_path("admin", []))
        return self.render_admin(path, config)

    @cherrypy.expose
    def add(self, *path, **kwargs):
        print "add:", path, kwargs
        config = self.load_config()
        config2 = get_config_path(config, path)
        template = get_view_info(config, path, "__view__").get("add_sections")
        max_sections = int(get_view_info(config, 
            path, "__view__").get("max_sections", 10))
        assert template, "Abnormal error: template missing"
        index = 0
        while 1:
            new_section = template % index
            if new_section not in config2.sections:
                break
            index += 1
        if len(config2.sections) >= max_sections:
            raise ValueError, "Max sections length reached (%d)" % max_sections
        config2[new_section] = {}
        config2.sections.sort(key= lambda s: re.match("(\w+)(\d+)", s).groups())
        config.validate(self.validator)
        config.write()
        self.run_write()        
        raise cherrypy.HTTPRedirect(build_path("admin", path + (new_section,)))

    @cherrypy.expose
    def remove(self, *path, **kwargs):
        print "remove:", path, kwargs
        config = self.load_config()
        config2 = get_config_path(config, path[:-1])
        remove_sections = get_view_info(config, 
            path[:-1], "__view__").get("remove_sections")
        assert remove_sections, "Abnormal error: remove_sections missing"
        del config2[path[-1]]
        config.validate(self.validator)
        config.write()
        self.run_write()
        raise cherrypy.HTTPRedirect(build_path("admin", path[:-1]))
                                      
    @cherrypy.expose
    def update(self, *path, **kwargs):
        print "update:", path, kwargs        
        config = self.load_config()
        for parameter, value in group_parameters(kwargs, config, path):
            info = get_validate_info(config, path, parameter)
            if info.type == "boolean" and isinstance(value, list):
                value = True
            elif info.type.endswith("_list") and not isinstance(value, list):
                value = [value]    
            if info.type.endswith("_list"):
                value = filter(bool, value)            
            subconfig = get_config_path(config, path)
            if value == "":
                del subconfig[parameter]
            else: 
                subconfig[parameter] = value
        
        validation = config.validate(self.validator)
        errors = [(sections + [key]) for (sections, key, error) in
            configobj.flatten_errors(config, validation)]
        if not errors:
            config.write()
            self.run_write()
            self.notice = "Configuration saved"
            raise cherrypy.HTTPRedirect(build_path("admin", path))
        else:        
            print "errors:", errors
            return self.render_admin(path, config, errors)
        
    @cherrypy.expose
    def unauthorized(self, *args, **kwargs):
        return "<h1>Access denied</h1>"
